Skip to content

feat!: adapt to evmlib PaymentVault API and simplify pricing#56

Merged
jacderida merged 20 commits intomainfrom
feat/adapt-evmlib-payment-vault
Apr 2, 2026
Merged

feat!: adapt to evmlib PaymentVault API and simplify pricing#56
jacderida merged 20 commits intomainfrom
feat/adapt-evmlib-payment-vault

Conversation

@mickvandijke
Copy link
Copy Markdown
Collaborator

Summary

  • Breaking: Replace QuotingMetrics with a single price: Amount field on PaymentQuote and MerklePaymentCandidateNode, matching the updated evmlib API
  • Breaking: Unify data_payments_address + merkle_payments_address into a single payment_vault_address (DevnetEvmInfo, CLI args)
  • Replace logarithmic capacity-based pricing with simple quadratic formula (n / 6000)² in wei — removes f64 arithmetic, uses U256 throughout
  • Verify single-node payments via completedPayments mapping directly instead of the removed verify_data_payment batch call
  • Remove zero_quoting_metrics() helper and all QuotingMetrics construction in tests

Note: Cargo.toml currently points evmlib at a local path (../evmlib) for development. This must be updated to the published crate version before merge.

Test plan

  • cargo test passes locally against local evmlib
  • Verify pricing formula produces expected values at key thresholds (0, 6000, 12000 records)
  • E2E merkle payment tests pass with the new candidate node structure
  • Single-node payment verification works against Anvil testnet
  • Update evmlib dependency to published crate before merge

🤖 Generated with Claude Code

mickvandijke added a commit that referenced this pull request Apr 1, 2026
Add unit and e2e tests covering the remaining Section 18 scenarios:

Unit tests (32 new):
- Quorum: #4 fail→abandoned, #16 timeout→inconclusive, #27 single-round
  dual-evidence, #28 dynamic threshold undersized, #33 batched per-key,
  #34 partial response unresolved, #42 quorum-derived paid-list auth
- Admission: #5 unauthorized peer, #7 out-of-range rejected
- Config: #18 invalid config rejected, #26 dynamic paid threshold
- Scheduling: #8 dedup safety, #8 replica/paid collapse
- Neighbor sync: #35 round-robin cooldown skip, #36 cycle completion,
  #38 snapshot stability mid-join, #39 unreachable removal + slot fill,
  #40 cooldown peer removed, #41 cycle termination guarantee,
  consecutive rounds, cycle preserves sync times
- Pruning: #50 hysteresis prevents premature delete, #51 timestamp reset
  on heal, #52 paid/record timestamps independent, #23 entry removal
- Audit: #19/#53 partial failure mixed responsibility, #54 all pass,
  #55 empty failure discard, #56 repair opportunity filter,
  response count validation, digest uses full record bytes
- Types: #13 bootstrap drain, repair opportunity edge cases,
  terminal state variants
- Bootstrap claims: #46 first-seen recorded, #49 cleared on normal

E2e tests (4 new):
- #2 fresh offer with empty PoP rejected
- #5/#37 neighbor sync request returns response
- #11 audit challenge multi-key (present + absent)
- Fetch not-found for non-existent key

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@mickvandijke mickvandijke marked this pull request as ready for review April 1, 2026 22:56
Copilot AI review requested due to automatic review settings April 1, 2026 22:56
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Updates ant-node to match the refactored evmlib PaymentVault API by removing QuotingMetrics from quotes/candidate nodes, simplifying pricing to a quadratic function, and adjusting on-chain verification to the new contract surface.

Changes:

  • Replace QuotingMetrics with a single price: Amount across quoting + merkle candidate flows, updating signing/verification and tests accordingly.
  • Switch payment verification to PaymentVault APIs (verifyPayment, completedPayments) and update merkle verification to read completed merkle payment info + validate paid amounts.
  • Replace the previous pricing logic with a U256-based quadratic pricing formula and update devnet manifest wiring to a unified payment_vault_address.

Reviewed changes

Copilot reviewed 9 out of 11 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
src/payment/verifier.rs Adapts EVM + merkle payment verification to the unified PaymentVault API and new on-chain data structures.
src/payment/single_node.rs Reworks SingleNode verification to use completedPayments and removes QuotingMetrics plumbing.
src/payment/quote.rs Generates quotes/candidate nodes using price (derived from record count) and updates signature bytes accordingly.
src/payment/pricing.rs Implements the new quadratic pricing formula in wei using Amount arithmetic and updates tests.
src/payment/proof.rs Updates proof tests to construct quotes/candidates with price instead of QuotingMetrics.
src/storage/handler.rs Adjusts merkle candidate quote request test expectations for the new candidate node structure.
tests/e2e/merkle_payment.rs Updates E2E merkle payment test helpers to build/sign candidate nodes with price.
src/devnet.rs Unifies devnet manifest EVM contract address fields into payment_vault_address.
src/bin/ant-devnet/main.rs Emits unified vault address into the devnet manifest from the Anvil network config.
Cargo.toml Points evmlib dependency to the refactor branch to pick up the new PaymentVault API.
Cargo.lock Locks the new git-sourced evmlib resolution.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +3 to +5
//! Uses the formula `(close_records_stored / 6000)^2` to calculate storage price.
//! Integer division means nodes with fewer than 6000 records get a ratio of 0,
//! but a minimum floor of 1 prevents free storage.
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The module docs claim that “integer division means nodes with fewer than 6000 records get a ratio of 0”, but calculate_price() scales before dividing (n² * 1e18 / 6000²), so prices for 1..5999 records are non-zero (and only n=0 hits the 1 wei floor). Please update the documentation to match the implemented formula/behavior.

Copilot uses AI. Check for mistakes.
Copilot AI review requested due to automatic review settings April 2, 2026 08:22
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 16 out of 18 changed files in this pull request and generated 5 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copilot AI review requested due to automatic review settings April 2, 2026 10:55
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 16 out of 18 changed files in this pull request and generated 1 comment.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copilot AI review requested due to automatic review settings April 2, 2026 14:21
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 16 out of 18 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copilot AI review requested due to automatic review settings April 2, 2026 16:24
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 16 out of 18 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

// Auto-scale: current DB footprint + available space − reserve.
let computed = compute_map_size(&env_dir, config.disk_reserve)?;
info!(
"Auto-computed LMDB map size: {:.2} GiB (available disk minus {:.2} GiB reserve)",
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The info log says the computed map size is "available disk minus reserve", but compute_map_size() actually adds the current data.mdb file size back in (current_db_file_size + (available - reserve)). Consider adjusting the message to reflect that the value includes existing DB footprint, so operators can interpret the number correctly.

Suggested change
"Auto-computed LMDB map size: {:.2} GiB (available disk minus {:.2} GiB reserve)",
"Auto-computed LMDB map size: {:.2} GiB (existing DB size + available disk minus {:.2} GiB reserve)",

Copilot uses AI. Check for mistakes.
Comment on lines 28 to 32
saorsa-pqc = "0.5"

# Payment verification - autonomi network lookup + EVM payment
evmlib = "0.5.0"
evmlib = "0.7"
xor_name = "5"
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR description notes evmlib is still on a local path and must be updated before merge, but Cargo.toml now points to the published evmlib = "0.7". Please update the PR description (or revert the dependency change if unintended) so the stated merge requirements match the actual diff.

Copilot uses AI. Check for mistakes.
mickvandijke and others added 13 commits April 2, 2026 21:29
The merkle payment verifier only checked that paid amounts were non-zero,
not that they met the candidate's quoted price. A malicious client could
submit fake low prices in PoolCommitment candidates while keeping the real
poolHash, causing the contract to charge almost nothing while nodes still
accepted the proof.

Replace `paid_amount.is_zero()` with `paid_amount < node.price` so each
paid candidate must receive at least their ML-DSA-65 signed quoted price.
Also fix existing unit tests that were missing the Amount field in
paid_node_addresses tuples, and add test_merkle_underpayment_rejected.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Migrate from the old QuotingMetrics-based pricing and split
DataPayments/MerklePayments contracts to the unified PaymentVault API
in evmlib. Key changes:

- Replace QuotingMetrics with a single `price: Amount` field on quotes
- Replace logarithmic pricing with simple quadratic formula (n/6000)²
- Unify data_payments_address + merkle_payments_address into
  payment_vault_address
- Verify payments via completedPayments mapping instead of
  verify_data_payment batch call

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The verifier checked `paid_amount >= node.price` (individual quote) but
the contract pays each winner `median16(quotes) * 2^depth / depth`. A
winner quoting above the median could be paid less than their quote,
causing the node to incorrectly reject a valid payment.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Integrate `SingleNodePayment::from_quotes` to derive correct on-chain payment amounts. This ensures exact-match checks in the contract's `verifyPayment` function pass by reconstructing amounts as used by the client.
Nodes should store as many chunks as disk allows. The LMDB map size
(db_size_gb) remains as the only storage bound.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Nodes no longer advertise or track a maximum record count. The
QuotingMetricsTracker API is simplified to only take initial_records.
The evmlib QuotingMetrics::max_records field is set to 0 since we
cannot remove it from the external type.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ics)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Previously, verify() checked each node's own on-chain payment amount.
Non-median nodes expected 0 and saw 0, so 4/5 nodes couldn't detect
underpayment. Now all nodes check the single median quote's on-chain
amount against the expected 3× price.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace the fixed 32 GiB LMDB map ceiling with dynamic sizing that
adapts to the storage partition. Nodes can now use all available disk
space for chunks without an artificial cap.

- Startup: map_size = current_db_size + available_space − reserve
- Resize-on-demand: when MDB_MAP_FULL is hit (e.g. operator added
  storage), re-query disk space, resize map, retry the write
- Disk-space guard: refuse writes when available space drops below
  a configurable reserve (default 1 GiB), with a 5-second TTL cache
  to avoid a statvfs syscall on every put
- Safety: resize never shrinks below env.info().map_size so existing
  data is never at risk
- New config: storage.disk_reserve_gb (default 1)
- Operator override: storage.db_size_gb > 0 still imposes a hard cap

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace the batch verifyPayment(DataPayment[5]) contract call with a
single completedPayments(quoteHash) lookup on just the median quote,
verifying it was paid at least 3x its price on-chain.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…tion

When multiple quotes share the median price, stable sort ordering may
differ between client and verifier, causing the verifier to check the
wrong quote hash on-chain. Now checks all tied quotes and accepts if
any one was paid correctly.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
mickvandijke and others added 4 commits April 2, 2026 21:30
…wards address generation

Add `#[allow(clippy::cast_possible_truncation)]` to clarify safe use of cast, as `i` is always < 5.
…trained runners

LmdbStorageConfig::default() sets disk_reserve to 1 GiB, which makes
tests depend on the host having that much free space. Add a test_default()
constructor with disk_reserve=0 and use it in all test/e2e configs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…empotency

Storing an already-present chunk could fail with "Insufficient disk space"
because the reserve check ran before the fast-path duplicate return. Since
duplicates don't write anything, the guard is unnecessary for them. Moving
it after the exists() check keeps duplicate puts as harmless no-ops.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@mickvandijke mickvandijke force-pushed the feat/adapt-evmlib-payment-vault branch from 432c9a5 to c3e136f Compare April 2, 2026 19:35
mickvandijke and others added 2 commits April 2, 2026 21:41
…unks removal

The `max_chunks` field was removed from `LmdbStorageConfig` but the test
helper in `audit.rs` still referenced it, causing a compilation failure.
Replace with `disk_reserve: 0` to match the current struct definition.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Nodes on smaller disks benefit from the reduced reserve while still
retaining enough headroom for OS operations, logs, and temp files.

- Rename config field `disk_reserve_gb` → `disk_reserve_mb` for finer
  granularity
- Extract `DEFAULT_DISK_RESERVE` and `MIB`/`GIB` byte constants
- Expose `storage::lmdb` as `pub(crate)` so `node.rs` can reuse `MIB`

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings April 2, 2026 19:55
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 18 out of 20 changed files in this pull request and generated 1 comment.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +79 to +86
/// A test-friendly default with `disk_reserve` set to 0 so unit tests
/// don't depend on the host having >= 1 GiB free disk space.
#[cfg(any(test, feature = "test-utils"))]
#[must_use]
pub fn test_default() -> Self {
Self {
disk_reserve: 0,
..Self::default()
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test_default() doc says tests otherwise depend on the host having ">= 1 GiB" free disk space, but the default disk_reserve is 500 MiB (and map sizing is independent of free space). Consider rewording this to accurately describe the dependency you're removing (i.e., bypassing the disk_reserve guard during tests).

Copilot uses AI. Check for mistakes.
Update payment workflows, tests, and comments to reflect the new group size. Adjusted median calculation logic and related constants accordingly.
@jacderida jacderida merged commit 7fce457 into main Apr 2, 2026
11 checks passed
@jacderida jacderida deleted the feat/adapt-evmlib-payment-vault branch April 2, 2026 23:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants